/*Paul West*/
#include "audio.h"
#include "thread.h"

void audio_thread(void *args);
void close_audio_thread();
static mutex_t *audio_mutex;
static void loadOGG(const char *fileName, sound_t *snd, ALsizei *sumBytes, ALenum *format, ALsizei *freq);
static int fill_snd_buffers(sound_t *snd, ALsizei *sumBytes, const int endian);

typedef struct _audio_list_t{
    sound_t *snd;
    struct _audio_list_t *next;
} audio_list_t;

audio_list_t audio_list;
static int add_snd(sound_t *snd);
static void remove_snd(sound_t *snd);

// Position of the source sound.
static ALfloat sourcePos[] = { 0.0, 0.0, 0.0 };
// Velocity of the source sound.
static ALfloat sourceVel[] = { 0.0, 0.0, 0.0 };
// Position of the Listener.
//static ALfloat listenerPos[] = { 0.0, 0.0, 0.0 };
// Velocity of the Listener.
//static ALfloat listenerVel[] = { 0.0, 0.0, 0.0 };
static ALCdevice *dev;
static ALCcontext *ctx;

void init_sound(){

    // Initialize OpenAL and clear the error bit.
    dev = alcOpenDevice(NULL);
    if(!dev){
        fprintf(stderr, "Oops\n");
        return;
    }
    ctx = alcCreateContext(dev, NULL);
    alcMakeContextCurrent(ctx);
    if(!ctx){
        fprintf(stderr, "Oops2\n");
        return;
    }
    // Clear Error Code (so we can catch any new errors)
    alGetError();
    audio_list.snd = NULL;
    audio_list.next = NULL;
    audio_mutex = create_mutex();
    create_and_run_thread((void*)&audio_thread, NULL, (void*)&close_audio_thread);
}

void close_sound(){
    alcMakeContextCurrent(NULL);
    alcDestroyContext(ctx);
    alcCloseDevice(dev);
}

sound_t* create_sound_t(const char *file, ALboolean is_looping, 
        ALboolean is_streaming, ALuint buffer_size){
    ALsizei sumBytes = 0;

    sound_t* snd = (sound_t*)malloc(sizeof(sound_t));
    if(snd == NULL){
        goto create_sound_fail;
    }
    snd->buffer_data = NULL;
    snd->buffer_size = buffer_size;
    snd->is_streaming = is_streaming;
    alGenBuffers(snd->is_streaming ? 2 : 1, snd->buf);
    alGenSources(1, &(snd->src));
    alSourcef (snd->src, AL_PITCH,    1.0      );
    alSourcef (snd->src, AL_GAIN,     1.0      );
    alSourcefv(snd->src, AL_POSITION, sourcePos);
    alSourcefv(snd->src, AL_VELOCITY, sourceVel);
    alSourcei (snd->src, AL_LOOPING,  is_looping ? !snd->is_streaming : 0);
    if(alGetError() == AL_NO_ERROR){
        loadOGG(file, snd, &sumBytes, &snd->format, &snd->freq);
        if(snd->buffer_data == NULL){
            goto create_sound_fail;
        }
        alBufferData(snd->buf[0], snd->format, (void*)snd->buffer_data, sumBytes, snd->freq);
        if(snd->is_streaming){
            sumBytes = 0;
            if(fill_snd_buffers(snd, &sumBytes, 0) < 0){
                goto create_sound_fail;
            }
            alBufferData(snd->buf[1], snd->format, (void*)snd->buffer_data, sumBytes, snd->freq);
            alSourceQueueBuffers(snd->src, 2, snd->buf);
            add_snd(snd);
        }else{
            alSourcei (snd->src, AL_BUFFER,   snd->buf[0]);
        }
        return snd;
    }
create_sound_fail:
    free_sound_t(&snd);
    return NULL;
}
void free_sound_t(sound_t **snd){
    remove_snd(*snd);
    if(snd != NULL && *snd != NULL){
        if(((*snd)->buffer_data) != NULL){
            free((*snd)->buffer_data);
        }
        if((*snd)->is_streaming){
            ov_clear(&((*snd)->ogg_file));
        }
        alDeleteSources(1, &((*snd)->src));
        alDeleteBuffers(1, &((*snd)->buf[0]));
        free(*snd);
    }
    *snd=NULL;
    alGetError();
}
void play_sound(sound_t *snd){
    if(snd !=NULL){
        alSourcePlay(snd->src);
    }
}
void stop_sound(sound_t *snd){
    if(snd !=NULL){
        alSourceStop(snd->src);
    }
}
void pause_sound(sound_t *snd){
    if(snd !=NULL){
        alSourcePause(snd->src);
    }
}

static int fill_snd_buffers(sound_t *snd, ALsizei *sumBytes, const int endian){
      long bytes;
      char *buffer = snd->buffer_data;
      int bit_stream;
      int bytes_left = snd->buffer_size;

      *sumBytes = 0;
      do {
          // Read up to a buffer's worth of decoded sound data
          //bytes = ov_read(&(snd->ogg_file), buffer+(*sumBytes), (snd->buffer_size)-(*sumBytes)-1, endian, 2, 1, &bit_stream);
          bytes = ov_read(&(snd->ogg_file), buffer+(*sumBytes), bytes_left-1, endian, 2, 1, &bit_stream);
          // Append to end of buffer
          //printf("bytes = %d(%d,%d,%d), left = %d\n", bytes, OV_HOLE, OV_EBADLINK, OV_EINVAL, bytes_left);
          bytes_left -= bytes;
          *sumBytes += bytes;
          if(snd->loop && bytes == 0 && bytes_left > 1020){
              ov_pcm_seek(&(snd->ogg_file), 0);
          }
      }while (bytes > 0 && bytes_left > 1020);
      buffer[*sumBytes] = 0;
      return *sumBytes;
}

static void loadOGG(const char *fileName, sound_t *snd, ALsizei *sumBytes, ALenum *format, ALsizei *freq){
      int endian = 0;             // 0 for Little-Endian, 1 for Big-Endian
      FILE *f;
      vorbis_info *pInfo;

      // Open for binary reading
      f = fopen(fileName, "rb");
      if(f==NULL || ov_open(f, &(snd->ogg_file), NULL, 0)){//no longer need to call fclose!
          goto load_ogg_failure;
      }
      // Get some information about the OGG file
      pInfo = ov_info(&(snd->ogg_file), -1);

      // Check the number of channels... always use 16-bit samples
      if (pInfo->channels == 1){
          *format = AL_FORMAT_MONO16;
      }else{
          *format = AL_FORMAT_STEREO16;
      }
      // The frequency of the sampling rate
      *freq = pInfo->rate;
      snd->buffer_data = malloc(snd->buffer_size);
      if(fill_snd_buffers(snd, sumBytes, endian) < 0){
          goto load_ogg_failure;
      }

      if(!snd->is_streaming){
          ov_clear(&(snd->ogg_file));//Again, fclose is called for us.
      }
      return;
load_ogg_failure:
      if(snd->buffer_data != NULL){
          free(snd->buffer_data);
          snd->buffer_data = NULL;
      }
      sumBytes = 0;
      return;
}
static int add_snd(sound_t *snd){
    audio_list_t *node;

    if(audio_list.snd == NULL){
        audio_list.snd = snd;
        return 1;
    }
    mutex_lock(audio_mutex);
    for(node = audio_list.next; node->next != NULL; node = node->next){;}
    node->next = (audio_list_t*)malloc(sizeof(audio_list_t));
    if(node->next == NULL){
        mutex_unlock(audio_mutex);
        return 0;
    }
    node->next->snd = snd;
    mutex_unlock(audio_mutex);
    return 1;
}

static void remove_snd(sound_t *snd){
    audio_list_t *node, *buf;

    mutex_lock(audio_mutex);
    if(audio_list.snd == snd){
        audio_list.snd = NULL;
        if(audio_list.next != NULL){
            audio_list.snd = audio_list.next->snd;
            audio_list.next = audio_list.next->next;
        }
        mutex_unlock(audio_mutex);
        return;
    }
    for(node = &audio_list; node->next != NULL && node->next->snd != snd; 
            node = node->next){;}
    if(node->next != NULL && node->next->snd == snd){
        buf = node->next;
        node->next = node->next->next;
        free(buf);
        mutex_unlock(audio_mutex);
        return;
    }
    mutex_unlock(audio_mutex);
}

static int running = 1;
void audio_thread(void *args){
    audio_list_t *node;
    sound_t *snd;
    int sumBytes;
    ALint val;

    while(running){
        mutex_lock(audio_mutex);
        for(node = &audio_list; node != NULL && node->snd != NULL;
                node = node->next){
            snd = node->snd;
            alGetSourcei(snd->src, AL_BUFFERS_PROCESSED, &val);
            if(val > 0){
                alSourceUnqueueBuffers(snd->src, 1, snd->buf);
                if(fill_snd_buffers(snd, &sumBytes, 0) < 0){
                    printf("Sound error!\n");
                }
                alBufferData(snd->buf[0], snd->format, (void*)snd->buffer_data, sumBytes, snd->freq);
                alSourceQueueBuffers(snd->src, 1, snd->buf);
                if(alGetError() != AL_NO_ERROR){
                    printf("Sound error!\n");
                }
            }
        }
        mutex_unlock(audio_mutex);
        //thread_yield();
        thread_sleep(1000*10);
    }
}

void close_audio_thread(){
    running = 0;
}
